package com.nineclock.attendance.service.impl;

import cn.hutool.core.date.DateUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nineclock.attendance.dto.AttendGroupDTO;
import com.nineclock.attendance.dto.AttendPunchDTO;
import com.nineclock.attendance.dto.AttendsDTO;
import com.nineclock.attendance.enums.AttendEnums;
import com.nineclock.attendance.excel.CustomHandler;
import com.nineclock.attendance.mapper.AttendPunchMapper;
import com.nineclock.attendance.pojo.AttendPunch;
import com.nineclock.attendance.service.AttendGroupService;
import com.nineclock.attendance.service.AttendPunchService;
import com.nineclock.common.enums.ResponseEnum;
import com.nineclock.common.exception.NcException;
import com.nineclock.common.filter.CurrentUserHolder;
import com.nineclock.common.utils.BeanHelper;
import com.nineclock.common.utils.DateTimeUtil;
import com.nineclock.common.utils.DistanceUtil;
import com.nineclock.system.dto.SysCompanyUserDTO;
import com.nineclock.system.feign.SysCompanyUserFeign;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Transactional
public class AttendPunchServiceImpl implements AttendPunchService {

    @Autowired
    private AttendGroupService attendGroupService;

    @Autowired
    private AttendPunchMapper attendPunchMapper;

    @Autowired
    private SysCompanyUserFeign companyUserFeign;


    @Override
    public void punch(AttendPunchDTO punchDTO) {
        // 1.参数校验
        if (punchDTO == null) {
            throw new NcException(ResponseEnum.INVALID_PARAM_ERROR);
        }
        // 2.封装打卡信息
        AttendPunch attendPunch = BeanHelper.copyProperties(punchDTO, AttendPunch.class);
        // 2.1 设置基本信息，员工id、公司id、创建时间
        attendPunch.setCompanyUserId(CurrentUserHolder.get().getCompanyUserId());
        attendPunch.setCompanyId(CurrentUserHolder.get().getCompanyId());
        attendPunch.setCreateTime(new Date());

        // 2.2 设置打卡时间（以服务端为准，否则用户可以在客户端修改手机时间打卡）
        Date now = new Date();
        attendPunch.setPunchTime(now);
        attendPunch.setPunchDateStr(DateTimeUtil.dateToStr(now, DateTimeUtil.TIME_FORMAT_2)); // 打卡日期（年-月-日）

        // 2.3 设置上午、下午打卡（1-上午，2-下午）
        attendPunch.setPunchOnOffWork(DateTimeUtil.noonType(now) + 1);

        // 2.4 查询员工所属的考勤组信息
        AttendGroupDTO attendGroupDTO = attendGroupService.getAttendGroupByUserId();
        if (attendGroupDTO == null) {
            throw new NcException(ResponseEnum.USER_NOT_MATCH_ATTENDGROUP);
        }

        // 2.5 判断是否有效范围内打卡，非有效范围抛出异常
        boolean areaValid = this.validatePunchArea(punchDTO, attendGroupDTO);
        attendPunch.setAreaValid(areaValid);

        // 2.6 判断是否是工作日打卡，非工作日抛出异常
        boolean workDayFlag = this.validateWorkDay(attendGroupDTO);
        if (!workDayFlag) {
            throw new NcException(ResponseEnum.PUNCH_INVALID_DAY);
        }

        // 2.7 设置打卡类型（打卡类型 1正常；2迟到；3早退；4旷工）
        this.setPunchType(attendPunch, attendGroupDTO);

        // 2.8 设置是否为有效打卡（上午以最早一次为有效，下午以最晚一次为有效）
        this.validateEffective(attendPunch);

        // 2.9 打卡来源
        attendPunch.setPunchSource(AttendEnums.PUNCH_SOURCE_PUNCH.value());

        attendPunchMapper.insert(attendPunch);
    }


    /**
     * 判断打卡类型 : 1正常；2迟到；3早退；4旷工
     *
     * @param attendPunch
     * @param attendGroupDTO
     */
    private void setPunchType(AttendPunch attendPunch, AttendGroupDTO attendGroupDTO) {
        //获取考勤组中的允许迟到早退分钟数,10
        Integer allowLateMinutes = attendGroupDTO.getAllowLateMinutes();

        //获取考勤组中的允许旷工分钟数,30
        Integer lateMinutes = attendGroupDTO.getLateMinutes();

        //获取实际打卡时间
        Date punchTime = attendPunch.getPunchTime();

        //获取实际打卡的时分秒
        String punchTimeStr = DateTimeUtil.dateToStr(punchTime, DateTimeUtil.TIME_FORMAT_1);

        //默认正常
        AttendEnums punchType = AttendEnums.PUNCH_TYPE_OK;

        //判定
        if (attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) { //上午
            //获取考勤组的设置上班时间
            String startWorkTime = attendGroupDTO.getStartWorkTime();

            //判定上班打卡时间
            //返回1 , 说明 : punchTimeStr（打卡时间） > startWorkTime（上班时间） + allowLateMinutes（允许迟到/早退的分数）
            if (DateTimeUtil.compareTime(punchTimeStr, startWorkTime, allowLateMinutes) == 1) {
                punchType = AttendEnums.PUNCH_TYPE_LATE; // 迟到

                //返回1 , 说明 : punchTimeStr > startWorkTime + lateMinutes
                if (DateTimeUtil.compareTime(punchTimeStr, startWorkTime, lateMinutes) == 1) {
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY; // 旷工
                }
            }

            attendPunch.setMorningPunchType(punchType.value());

        } else { //下午
            String offWorkTime = attendGroupDTO.getOffWorkTime();

            //判定下班打卡时间
            //返回1 , 说明 : offWorkTime > punchTimeStr + allowLateMinutes
            if (DateTimeUtil.compareTime(offWorkTime, punchTimeStr, allowLateMinutes) == 1) {
                punchType = AttendEnums.PUNCH_TYPE_EARLY; // 早退

                //返回1 , 说明 : offWorkTime > punchTimeStr + lateMinutes
                if (DateTimeUtil.compareTime(offWorkTime, punchTimeStr, lateMinutes) == 1) {
                    punchType = AttendEnums.PUNCH_TYPE_STAYALAY;  // 旷工
                }
            }
            attendPunch.setAfternoonPunchType(punchType.value());
        }
    }

    /**
     * 判断上下午打卡是否有效打卡
     *
     * @param attendPunch
     * @return
     */
    private void validateEffective(AttendPunch attendPunch) {

        //根据条件查询数据库中当前用户的今日上午/下午 打卡记录
        LambdaQueryWrapper<AttendPunch> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AttendPunch::getCompanyUserId, attendPunch.getCompanyUserId()) //打卡员工ID
                .eq(AttendPunch::getPunchDateStr, attendPunch.getPunchDateStr()) //打卡日期，年-月-日
                .eq(AttendPunch::getPunchOnOffWork, attendPunch.getPunchOnOffWork()) //上午，下午
                .eq(AttendPunch::getAreaValid, true) //打卡范围有效
                .eq(AttendPunch::getPunchSource, AttendEnums.PUNCH_SOURCE_PUNCH.value()); // 打卡来源


        Integer punchCount = attendPunchMapper.selectCount(queryWrapper);
        //如果未获取到, 说明没有打过卡, 此次打卡, 无论是上午还是下午都是有效的
        if (punchCount == 0) {
            attendPunch.setEffectiveValid(true);
        } else {
            //如果获取到记录, 说明已经打过卡
            //如果是上午, 第一条有效, 其余都是无效的
            if (attendPunch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) { // 上午
                attendPunch.setEffectiveValid(false);

            } else { //如果是下午, 最后一条有效, 其余都是无效的
                attendPunch.setEffectiveValid(true);
                //更新之前的打卡记录为无效, 以最后一次打卡为准
                AttendPunch ap = new AttendPunch();
                ap.setEffectiveValid(false);
                attendPunchMapper.update(ap, queryWrapper);
            }
        }
    }

    /**
     * 校验工作日
     *
     * @param attendGroupDTO
     * @return
     */
    private boolean validateWorkDay(AttendGroupDTO attendGroupDTO) {
        // 使用hutool的工具类，获取今天是周几
        int week = DateUtil.thisDayOfWeek();   //1: 表示周日, 2: 表示周一, 3:表示周二
        List<String> workdays = attendGroupDTO.getWorkdays(); // [1,1,1,1,1,0,1]

        // 获取在集合中的下标
        if (week == 1) {
            week = 6;
        } else {
            week = week - 2;
        }

        Integer value = Integer.valueOf(workdays.get(week)); // 获取workdays集合元素
        return value == 1;
    }

    /**
     * 校验打卡范围
     *
     * @param attendPunchDTO
     * @param attendGroupDTO
     * @return
     */
    private boolean validatePunchArea(AttendPunchDTO attendPunchDTO, AttendGroupDTO attendGroupDTO) {
        //比较"打卡坐标" 和 "考勤组打卡坐标"的距离
        Integer meter = DistanceUtil.getDistanceMeter(attendPunchDTO.getLat().doubleValue(), attendPunchDTO.getLng().doubleValue(), attendGroupDTO.getLat().doubleValue(), attendGroupDTO.getLng().doubleValue());
        //判定是否小于等于考勤组中设置的范围
        return meter <= attendGroupDTO.getAddressRange();
    }


    @Override
    public List<AttendPunchDTO> queryMembers(String startTime, String endTime) {
        // 获取两个日期之间的日期集合
        List<String> spanDateList = DateTimeUtil.getSpanDateList(startTime, endTime);

        // 获取当前企业的所有员工列表
        List<SysCompanyUserDTO> companyUserDTOList = companyUserFeign.queryAllCompanyUser().getData();

        return getAttendPunchDTOS(startTime, endTime, spanDateList, companyUserDTOList);
    }

    /**
     * 公共方法，获取考勤数据
     * @param startTime
     * @param endTime
     * @param spanDateList
     * @param companyUserDTOList
     * @return
     */
    private List<AttendPunchDTO> getAttendPunchDTOS(String startTime, String endTime, List<String> spanDateList, List<SysCompanyUserDTO> companyUserDTOList) {
        if (!CollectionUtils.isEmpty(companyUserDTOList)) {

            List<AttendPunchDTO> attendPunchDTOList = companyUserDTOList.stream().map(companyUserDTO -> {

                AttendPunchDTO attendPunchDTO = BeanHelper.copyProperties(companyUserDTO, AttendPunchDTO.class);

                // 查询该员工在某一段时间内的打卡情况
                LambdaQueryWrapper<AttendPunch> wrapper = new LambdaQueryWrapper<>();
                wrapper.eq(AttendPunch::getCompanyUserId, companyUserDTO.getId());
                wrapper.eq(AttendPunch::getEffectiveValid, true);
                wrapper.between(AttendPunch::getPunchDateStr, startTime, endTime);

                // 包含该员工某段时间内所有的有效打卡记录
                List<AttendPunch> attendPunchList = attendPunchMapper.selectList(wrapper);

                // 将打卡记录进行分组，按照日期分组，key为每一天的日期，value为每一天上午、下午的有效打卡记录（最多两条）
                Map<String, List<AttendPunch>> attendPunchMap = attendPunchList.stream().collect(Collectors.groupingBy(AttendPunch::getPunchDateStr));


                List<AttendsDTO> attendsDTOList = spanDateList.stream().map(everyDay -> {

                    AttendsDTO attendsDTO = new AttendsDTO();
                    attendsDTO.setAttendDate(everyDay);

                    // 获取每一天的上午、下午的打卡情况
                    List<AttendPunch> punchList = attendPunchMap.get(everyDay);

                    if (CollectionUtils.isEmpty(punchList)) {
                        attendsDTO.setAttendStatus("缺卡/缺卡");
                    } else {
                        String attendStatus = this.handleAttendStatus(punchList);
                        attendsDTO.setAttendStatus(attendStatus);
                    }

                    return attendsDTO;

                }).collect(Collectors.toList());

                attendPunchDTO.setAttendsList(attendsDTOList);

                return attendPunchDTO;

            }).collect(Collectors.toList());

            return attendPunchDTOList;
        }
        return null;
    }

    /**
     * 处理打卡状态
     * @param punches
     * @return
     */
    private String handleAttendStatus(List<AttendPunch> punches) {
        String amAttendStatus = "缺卡"; //上午 - 考勤
        String pmAttendStatus = "缺卡"; //下午 - 考勤

        for (AttendPunch punch : punches) {
            if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_MORNING.value()) {
                amAttendStatus = this.getPuchTypeDesc(punch.getMorningPunchType());
            } else if (punch.getPunchOnOffWork() == AttendEnums.NOON_TYPE_AFTERNOON.value()) {
                pmAttendStatus = this.getPuchTypeDesc(punch.getAfternoonPunchType());
            }
        }
        return amAttendStatus + "/" + pmAttendStatus;
    }

    /**
     * 根据打卡类型，获取打卡类型描述
     */
    private String getPuchTypeDesc(Integer punchType) {
        String desc = "";
        switch (punchType) {
            case 1:
                desc = AttendEnums.PUNCH_TYPE_OK.toString();
                break;
            case 2:
                desc = AttendEnums.PUNCH_TYPE_LATE.toString();
                break;
            case 3:
                desc = AttendEnums.PUNCH_TYPE_EARLY.toString();
                break;
            case 4:
                desc = AttendEnums.PUNCH_TYPE_STAYALAY.toString();
                break;
            case 5:
                desc = AttendEnums.PUNCH_TYPE_NOT_WORK_DAY.toString();
                break;
            default:
                break;
        }
        return desc;
    }


    @Override
    public void export(String startTime, String endTime) throws Exception{
        // 查询考勤数据
        List<AttendPunchDTO> attendPunchDTOList = this.queryMembers(startTime, endTime);

        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = servletRequestAttributes.getResponse();
        response.setHeader("Content-disposition", "attachment;filename=Attendance.xls");
        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setCharacterEncoding("utf-8");

        // 生成excel文件，下载
        EasyExcel.write(response.getOutputStream())
                .excelType(ExcelTypeEnum.XLS)
                .registerWriteHandler(this.horizontalCellStyleStrategy())
                .registerWriteHandler(new CustomHandler())
                .sheet("考勤报表")
                .head(this.handleHeaders(attendPunchDTOList))
                .doWrite(this.handleData(attendPunchDTOList));

    }

    /**
     * 动态构造每一行数据
     * @param attendPunchDTOList
     * @return
     */
    private List<List<String>> handleData(List<AttendPunchDTO> attendPunchDTOList) {
        List<List<String>> dataList = new ArrayList<>();

        if(!CollectionUtils.isEmpty(attendPunchDTOList)){
            // 遍历每一行数据
            for (AttendPunchDTO attendPunchDTO : attendPunchDTOList) {
                List<String> row = new ArrayList<>();
                row.add(attendPunchDTO.getUserName());
                row.add(attendPunchDTO.getWorkNumber());
                row.add(attendPunchDTO.getDepartmentName());
                row.add(attendPunchDTO.getPost());

                List<AttendsDTO> attendsList = attendPunchDTO.getAttendsList();
                if(!CollectionUtils.isEmpty(attendsList)){
                    for (AttendsDTO attendsDTO : attendsList) {
                        row.add(attendsDTO.getAttendStatus());
                    }
                }
                dataList.add(row);
            }
        }
        return dataList;
    }

    /**
     * 动态构造表头
     * @param attendPunchDTOList
     * @return
     */
    private List<List<String>> handleHeaders(List<AttendPunchDTO> attendPunchDTOList) {
        List<List<String>> headers = new ArrayList<>();

        headers.add(Arrays.asList("姓名"));
        headers.add(Arrays.asList("工号"));
        headers.add(Arrays.asList("部门"));
        headers.add(Arrays.asList("职位"));

        if(!CollectionUtils.isEmpty(attendPunchDTOList)){
            AttendPunchDTO attendPunchDTO = attendPunchDTOList.get(0);// 取出第一条数据

            List<AttendsDTO> attendsList = attendPunchDTO.getAttendsList();

            if(!CollectionUtils.isEmpty(attendsList)){

                for (AttendsDTO attendsDTO : attendsList) {
                    headers.add(Arrays.asList(attendsDTO.getAttendDate()));
                }
            }

        }
        return headers;
    }


    /**
     * 设置样式
     */
    public HorizontalCellStyleStrategy horizontalCellStyleStrategy() {

        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //内容策略 - 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

        //设置字体
        WriteFont font = new WriteFont();
        font.setFontName("阿里巴巴普惠体");
        font.setFontHeightInPoints((short) 11);
        contentWriteCellStyle.setWriteFont(font);
        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(contentWriteCellStyle, contentWriteCellStyle);

        return horizontalCellStyleStrategy;
    }


    @Override
    public String generatePunchReport(Long companyId) {
        // 获取本月第一天和最后一天
        String startTime = DateTimeUtil.getCurrentMonthFirstDay();
        String endTime = DateTimeUtil.getCurrentMonthLastDay();

        // 获取两个日期之间的日期集合
        List<String> spanDateList = DateTimeUtil.getSpanDateList(startTime, endTime);

        // 获取企业的员工列表
        List<SysCompanyUserDTO> companyUserDTOList = companyUserFeign.queryAllUserByCompanyId(companyId).getData();

        // 调用公共方法，获取考勤数据
        List<AttendPunchDTO> punchDTOList = this.getAttendPunchDTOS(startTime, endTime, spanDateList, companyUserDTOList);

        // 指定文件路径，D:\考勤数据\公司id
        String folder = "D:" + File.separator + "考勤数据" +  File.separator + companyId;
        File file = new File(folder);

        if(!file.exists()){
            // 如果文件夹不存在，创建文件夹
            file.mkdirs();
        }

        // D:\考勤数据\公司id\公司名称-2021-11-考勤数据.xls
        String fileName = folder + File.separator + companyUserDTOList.get(0).getCompanyName()
                + "-" + startTime.substring(0, 7) + "-考勤数据.xls";

        EasyExcel.write(new File(fileName))
                .excelType(ExcelTypeEnum.XLS)
                .registerWriteHandler(this.horizontalCellStyleStrategy())
                .registerWriteHandler(new CustomHandler())
                .sheet("考勤报表")
                .head(this.handleHeaders(punchDTOList))
                .doWrite(this.handleData(punchDTOList));

        return fileName;
    }

}
